/**
 * ======================================================================
 * 插件作用：实现在元素的 class 属性中直接合并 windicss 原子类
 * ----------------------------------------------------------------------
 * Author: 曹操<pgcao@qq.com>(https://gitee.com/pgcao) License MIT
 * ======================================================================
 * see https://gitee.com/pgcao/windicss-elements-apply
 */

const vitePluginWindicssApply = (options = {}) => {
  var regexClassSplitter = /[\s'"`{}]/g
  var regexClassCheck1 = /^!?[a-z\d@<>.+-](?:\([\w,.%#\(\)+-]*\)|[\w:/\\,%#\[\].$-])*$/
  var regexClassCheck2 = /[a-z].*[\w)\]]$/
  var regexClassChecks = [regexClassCheck1, regexClassCheck2]

  var formatReg = /<([\w]+)\s*[^<,>]*(?=class\s*?=\s*?((['"])*[^'"]*))[^<,>]*\/?>/gi
  var applyReg = /^@([a-z]+)(?:([a-z0-9-_]+))?(\s*\(([^)]*)\))?/i

  async function hashCode(str) {
    return new Promise(resolve => {
      str = str.replace(/\s*\r?\n/g, '')
      let hash = 5381
      let i = str.length
      while (i--) hash = ((hash << 5) - hash) ^ str.charCodeAt(i)
      let hashStr = (hash >>> 0).toString(36)
      resolve(hashStr)
    })
  }

  async function replaceAsync(str, regex, asyncFn) {
    const promises = []
    str.replace(regex, (match, ...args) => {
      const promise = asyncFn(match, ...args)
      promises.push(promise)
    })
    const data = await Promise.all(promises)
    return str.replace(regex, () => data.shift())
  }

  function validClassName(i) {
    return regexClassChecks.every(r => i.length > 2 && i.match(r))
  }

  async function supportStyleName(source, id, isStart) {
    const styleKey = 'css'
    const styleCode = new Map()
    const classNum_key = await hashCode(id)

    let classIndex = 0

    // 取出带有class属性的元素标签头
    const formatFun = async (elements, tags, classValues, marks) => {
      if (!classValues.includes('@apply')) return elements

      let [classNames, ...Others] = classValues.substring(1).split(';')
      let ApplyOther_after = Others.join(' ')

      let classNamesArr = classNames.split(regexClassSplitter).filter(validClassName)
      if (!classNamesArr.length) return elements

      let ApplyOther_befor = ''
      let ApplyName = classNamesArr.shift()

      if (!ApplyName.includes('@apply')) {
        ApplyName = classNames
      }

      const applyArr = applyReg.exec(classNames) || []
      if (applyArr.length < 5 || applyArr[1] !== 'apply') {
        return elements
      }

      ;(ApplyName = applyArr[2]), (ApplyOther_befor = applyArr[4])

      let validClassNames = classNamesArr.join(' ')
      ApplyName = ApplyName ? ApplyName.replace('-', '') : classNum_key + '-' + classIndex

      let newClassNames = styleKey + '-' + ApplyName
      styleCode.set(newClassNames, `@apply ${validClassNames};`)

      const newClassValues = marks + (ApplyOther_befor ? ApplyOther_befor + ' ' : '') + newClassNames + (ApplyOther_after ? ' ' + ApplyOther_after : '')

      classIndex++

      return elements.replace(classValues, newClassValues)
    }

    source = await replaceAsync(source.trimStart(), formatReg, formatFun)

    const styleStr = new Array()
    for (const [key, val] of styleCode) {
      styleStr.push(`.${key}{\n${val};\n}`)
    }

    if (styleStr.length > 0) {
      source = `<style>${styleStr.join('\n')}</style>\r\n` + source
    }

    return source
  }

  return [
    {
      name: 'vite-plugin-windicss-elements-apply',
      enforce: 'pre',
      // 请求模块时处理文件
      async transform(source, id) {
        if (!/\.vue$/.test(id)) {
          return null
        }
        try {
          return await supportStyleName(source, id, true)
        } catch (e) {
          this.error(e)
        }
        return null
      },

      // 执行自定义 HMR 更新处理
      async handleHotUpdate(ctx) {
        if (!/\.vue$/.test(ctx.file)) {
          return null
        }
        const defaultRead = ctx.read
        ctx.read = async function () {
          return await supportStyleName(await defaultRead(), ctx.file, false)
        }
      }
    },
    {
      name: 'vite-plugin-windicss-elements-apply:css',
      enforce: 'post',
      // 去掉 css sourcemap 防止控制台提示
      // Sourcemap for ****** points to missing source files
      async transform(source, id) {
        if (!/vue&type=style/.test(id)) {
          return null
        }
        return {
          code: source,
          map: {}
        }
      }
    }
  ]
}

export { vitePluginWindicssApply }
